avatar

目录
329 - 跨文件命名空间

跨文件命名空间

🧑‍💻 User

In this lecture, we’re going to learn about namespaces across multiple files, and we are going tobe using three classes to illustrate our point here.The first class is going to be a point class, which is going to model a point in our C plus plus program.If you look here, you’re going to see that the class is enclosed in a namespace.These are the curly braces delimiting our namespace and the class is going to live inside and this isthe setup you can do.Please note that what we have here is a header file and this is the definition of the class.But it is also possible to put some definitions for our member functions in an implementation file ora CPP file.And this is how we can do it.If you look here, we have our namespace here and inside the namespace we have whatever we need to putin our class definition.We have the definition for the constructors and we have a destructor here and this is the setup youreally use.If we go down, we’re going to see our line class.A line is modeled as a pair of points.We have a starting point, we have an ending point.And if we try to connect these two points, we’re going to get a line.This is the logic behind how we set up our class here.We have a constructor and we have a function to print information about our line.And we’re going to print information about the starting point and the ending point here.Nothing complicated here.We also have an implementation file for our line and inside our namespace we’re going to have the implementationfor the constructor here just to have some fun.We’re also going to put in a cylinder class.The cylinder class is also going to leave in a namespace as you see here, and it is going to be enclosedby our namespace.And in here we have the definition for our class.We also have a CPP file for this and we’re going to have the definition for our constructor inside.Once we have our namespaces and classes set up like this, we can use them in the main function.And here is a simple example of how you can do that.We’re going to include our classes, as you see here, and then before we use any constructor, we’regoing to prefix that with a namespace name.You see here we are saying we have to put the colon colon to mean that we want to call something thatlives in that namespace.Then we’re going to do whatever we did before.We have a point class, we create an object of it and the object is going to be created.We can also create a second object, as you see here, and we can print the information.And this is really good.We can also create a line object, as you see here.We can print information about that.We can create a cylinder object and call stuff on that object.And this is really going to work.Now that you have seen this, we’re going to head over to Visual Studio Code and play with us a littlemore.Okay.Here we are in our working folder of the current project is namespaces across multiple files.We’re going to grab our template files pretty quick and we’re going to put them in place and then we’regoing to open this in Visual Studio code by dragging and dropping like we do here, we’re going to openup our main CPP file, close the left pane here, or let’s let it open because we we’re going to useit in a minute.Let’s clean up a little bit and we’re going to create our point class.We’re going to create a header file.We are going to create a CPP file.I am going to put in the code for the header file here.Nothing complicated.You see, we are setting up a namespace delimited by these curly braces on line six and 22 and theninside we have our point class.We have our member variables X and Y.We have the constructor, which is going to be taking two parameters and we have a printing functionwhich is going to print information about our point here.Nothing complicated so far.We also have a CPP file as you see here.Again, we have the namespace and we have our stuff inside the namespace.Please note that we have the same namespace across multiple files and these are not going to be differentnamespaces.The namespace we have in the CPP file, as you see here, is going to be an extension of the same namespacewe have in the header file.Please remember that just because they live in different files doesn’t mean they are different namespaces.They are still the same namespace.So let’s create our line class.Now that the point is done, we’re going to say line dot h for the header, we’re going to say linedot cpp for the CPP file.We are going to put in the code for the header file for.Our line class again, nothing complicated.We have our namespace and inside we have the code for the line class, the declaration and the definitionof our class.Here we are going to hop over to the CPP file and put in the code for that.And in case I didn’t mention it, if you don’t want to type all these things I am doing here, you canuse the code attached to the resource section of the lecture that you are watching right now.So that’s going to make things a little easy.If you don’t want to type this, you’re going to have the code here ready to use.Okay, we have our line class in place.We’re going to create our cylinder class.Let’s do the header file, cylinder dot h, and we’re going to do cylinder dot CPP.We are going to put in the code for a cylinder.Let’s do that.You can see the code here if you want to type this and we are going to put in the implementation forour cylinder class here.This is just the definition of our constructor and it is enclosed in our namespace here.Now that we have everything set up, now we can go in main and use these things.The first thing we need to do in Main is to include our classes.We’re going to include our point class.Let’s do that point dot H, we’re going to include the line class and we’re going to include the cylinderclass.Okay, we’re done here.Now that we have this, we can head over in the main function and use this things.The first thing we’re going to do is to create a point object.So usually we use to say point P one, for example, and say 10 or 20 here, and this is what we did.But you see that this is not working because the point class isn’t defined in the global namespace.If we go in the point header, we’re going to see that it lives in a namespace called Geom.And if we want to create an object of this guy, we need to prefix it with the namespace name.We’re going to say Geom and we’re going to put a colon.Colon.This is going to be valid because now the compiler knows where to find this class here.And this is really cool.We can create another object.Let’s do that just for the fun of it.Geom And we’re going to say point and we’re going to say p two here.Let’s use lowercase and we’re going to say 3.5 or 4 and 6.1.Why not?So we can do something like this.Let’s use a dot here.And now that we have the objects, we can really do anything we want.We can print information about our point.I think we have a method like that in our point class.Yes, we have.So we’re going to say p two and say print eval and we can do something like this.If we build and run this program, we’re going to build with GCC.Let’s do that.We are going to see that the world is good.As you see here.Let’s close the left pane.We don’t need it anymore.We can bring up a terminal so that we can build this little guy and clear and run rooster.And we’re going to see information about our points.And this is really cool.We can also create line objects because we have a line class, as we see here.So let’s do that.We’re going to put a separator hereand we’re going to say line and we’re going to say Geom and we’re going to create a line object andwe’re going to say L one and let’s use P one and P two to create our line object.We can do that because if you go to the constructor of our line object, you’re going to see that it’sgoing to take two points by reference here.So we’re going to pass our two points and this is going to work.We can print information about a line object.As you see here, we have a member function to print information.So let’s do that.We’re going to say L one printable and we’re going to print information about this little guy.We can build and run this program.Let’s use GCC to do that.The world is going to be good, as you see here.And if we hit enter, we’re going to clear, clear and run Rooster.This is going to show information about our point.The line is going from 0.1, which is this guy here to point two, which is what we have in our codehere.And this is really cool.We can copy this separator and create a cylinder object just to show you that you can play with thisthings a little more.We’re going to create a cylinder object.We’re going to go through the namespace where the cylinder class is living and the namespace happensto be geom.So that’s what we’re going to use.We’re going to say Golem and we’re going to say cylinder and we’re going to say C one And what do weneed to pass to our cylinder when we create it?We need to pass the base radius and the height.Let’s pass the base radius as 1.4 and the height to be ten.Why not?After we have this thing declared, we’re going to call it.Maybe we can print the volume.Let’s see if we have a member function called volume.I think we do.Yes, we do.We are going to say volume and we’re going to say C one volume and this is going to give us the volumeof our cylinder here.We can build and run this little guy.So let’s build with GCC.The build is going to be good.We can clear and run Rooster and we’re going to see that our volume is in here.And if you replace these values and and compute the volume of our cylinder here, this is what you’regoing to get.This is really all I wanted you to see in this lecture that you can set up namespaces across multiplefiles.There is really nothing complicated.All you have to do is specify the namespace using the syntax here and you’re going to put whatever youwant inside that namespace.Namespaces can span multiple files.This is the message here.For example, we have the Geom namespace spanning the header and the CPP file and all you have to doto use the namespace as we have seen is to prefix what you want to call with the namespace name andput colon.Call on.As you see here, we are going to stop here in this lecture.In the next one, we’re going to learn about the global namespace.Go ahead and finish up here and meet me there.


🤖 Assistant

好的,作为您的C++专业开发导师和最佳老师,我将用最清晰、最全面的方式来帮助您学习C++中的跨文件命名空间


C++跨文件命名空间 (Namespaces Across Multiple Files) 详解

简介: 本节课我们将深入学习C++中如何使用命名空间来组织代码,特别是当代码分散在多个头文件实现文件 (.cpp文件)时。通过实例,您将掌握命名空间在大型项目中的重要作用及其使用方法。


1. 什么是命名空间 (Namespace)?为什么需要它?

在C++编程中,随着项目的增大,您可能会定义很多、函数和变量。不同的库或者您自己的不同模块可能定义了同名的实体(比如,您的几何库中有一个 Point 类,而另一个图形库中可能也有一个 Point 类)。这时就会出现名称冲突 ([name collision])。

命名空间就是C++为了解决这个问题而引入的一个机制。您可以把它想象成一个“姓氏”或者一个“公司的前缀”。通过把相关的代码实体(比如类、函数、变量)放在一个特定的命名空间内,可以有效地避免不同模块间的名称冲突。

例如:

  • 您定义的 Point 类可以放在 MyProject::Geom 命名空间里。
  • 别人定义的 Point 类可能在 GraphicsLib::Shapes 命名空间里。
    这样,当您需要使用时,就可以明确指定您想要使用的 Point 是哪一个,例如 MyProject::Geom::PointGraphicsLib::Shapes::Point

2. 跨文件命名空间 (Namespaces Across Multiple Files) 的实现

这是本节课的核心内容。在实际开发中,我们通常会将类的声明放在头文件中,而实现(成员函数的定义)放在对应的.cpp文件中。命名空间可以很好地跨越这些文件。

核心思想:
当您在不同的文件中使用相同名称命名空间时,C++编译器会将其视为同一个命名空间,并将其内容进行合并 ([merging])。这意味着您可以在一个头文件中声明一个命名空间内的类,然后在对应的.cpp文件中继续定义这个命名空间内的成员函数,而它们仍然属于同一个命名空间。

我们将使用三个示例类:Point(点)、Line(线)和Cylinder(圆柱体)来演示这个概念,它们都将位于一个名为 Geom 的命名空间中。

文件结构示例:

Code
1
2
3
4
5
6
7
8
MyProject/
├── point.h
├── point.cpp
├── line.h
├── line.cpp
├── cylinder.h
├── cylinder.cpp
└── main.cpp

2.1 Point 类示例

point.h (头文件 - 声明)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef GEOM_POINT_H // 防止头文件被重复包含的宏
#define GEOM_POINT_H

// 声明一个名为 Geom 的命名空间
namespace Geom {
// Point 类定义在 Geom 命名空间内
class Point {
private:
double m_x; // X 坐标
double m_y; // Y 坐标
public:
// 构造函数 (Constructor) 声明
Point(double x_coord = 0.0, double y_coord = 0.0);
// 打印点信息的函数声明
void print_info() const;

// 获取 x 坐标的函数
double get_x() const { return m_x; }
// 获取 y 坐标的函数
double get_y() const { return m_y; }
};
} // 命名空间 Geom 结束

#endif // GEOM_POINT_H

解释:

  • 我们首先使用 #ifndef/#define/#endif 预处理指令来防止头文件被多次包含 ([include guard])。
  • namespace Geom { ... } 声明了一个名为 Geom命名空间
  • Point 的声明(包括成员变量和成员函数)都放置在 Geom 命名空间内部。

point.cpp (实现文件 - 定义)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "point.h" // 包含 Point 类的头文件
#include <iostream> // 用于输出

// 再次打开 Geom 命名空间。这并不会创建新的 Geom 命名空间,
// 而是继续向之前在 point.h 中声明的 Geom 命名空间添加内容。
namespace Geom {
// Point 类的构造函数定义
Point::Point(double x_coord, double y_coord)
: m_x(x_coord), m_y(y_coord) {
// 可以在这里添加一些构造时的输出,用于调试
// std::cout << "Point constructor called!" << std::endl;
}

// Point 类的 print_info 成员函数定义
void Point::print_info() const {
std::cout << "Point: [x: " << m_x << ", y: " << m_y << "]" << std::endl;
}
} // 命名空间 Geom 结束

解释:

  • .cpp文件中,我们再次使用 namespace Geom { ... }重点是:这不会创建一个新的 Geom 命名空间,而是扩展(或者说“打开”)了在 point.h 中已经声明的那个 Geom 命名空间。 它们是同一个 Geom 命名空间。
  • 当定义 Point 类的成员函数时,由于 Point 类本身在 Geom 命名空间内,所以我们需要使用 Geom::Point:: 来指定我们正在定义的是 Geom 命名空间中的 Point 类的成员函数。这里的 ::作用域解析运算符,它告诉编译器 Point 位于 Geom 命名空间内。

2.2 Line 类示例

Line 类由两个 Point 对象组成,因此它需要包含 point.h

line.h

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef GEOM_LINE_H
#define GEOM_LINE_H

#include "point.h" // Line 类需要使用 Point 类

namespace Geom {
class Line {
private:
Point m_start_point; // 起点
Point m_end_point; // 终点
public:
Line(const Point& start, const Point& end); // 构造函数声明
void print_info() const; // 打印线信息的函数声明
};
} // 命名空间 Geom 结束

#endif // GEOM_LINE_H

line.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "line.h"
#include <iostream>

namespace Geom {
Line::Line(const Point& start, const Point& end)
: m_start_point(start), m_end_point(end) {
// std::cout << "Line constructor called!" << std::endl;
}

void Line::print_info() const {
std::cout << "Line: From ";
m_start_point.print_info(); // 调用 Point 类的 print_info
std::cout << " To ";
m_end_point.print_info(); // 调用 Point 类的 print_info
}
} // 命名空间 Geom 结束

注意:line.cpp 中定义 Line 类的成员函数时,我们直接使用了 Point 类型,因为 PointLine 都被包含在 Geom 命名空间中。在同一个命名空间内部,可以直接访问该命名空间内的其他实体,无需再次使用 Geom:: 前缀。

2.3 Cylinder 类示例

Cylinder 类也同样位于 Geom 命名空间中。

cylinder.h

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef GEOM_CYLINDER_H
#define GEOM_CYLINDER_H

namespace Geom {
class Cylinder {
private:
double m_base_radius; // 底面半径
double m_height; // 高度
public:
Cylinder(double radius = 1.0, double height = 1.0); // 构造函数声明
double volume() const; // 计算体积的函数声明
};
} // 命名空间 Geom 结束

#endif // GEOM_CYLINDER_H

cylinder.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "cylinder.h"
#include <iostream>
#include <cmath> // 用于 M_PI

// 检查是否定义了 M_PI,如果没有,手动定义一个近似值
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

namespace Geom {
Cylinder::Cylinder(double radius, double height)
: m_base_radius(radius), m_height(height) {
// std::cout << "Cylinder constructor called!" << std::endl;
}

double Cylinder::volume() const {
return M_PI * m_base_radius * m_base_radius * m_height;
}
} // 命名空间 Geom 结束

3. 在 main 函数中使用这些类

现在我们已经定义了所有类,并把它们放在了 Geom 命名空间中。接下来看看如何在主函数中实际使用它们。

main.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 包含所有需要使用的头文件
#include <iostream>
#include "point.h"
#include "line.h"
#include "cylinder.h"

int main() {
std::cout << "--- 使用 Geom::Point ---" << std::endl;
// 创建 Point 对象。由于 Point 在 Geom 命名空间内,
// 必须使用 Geom::Point 来明确指定。
Geom::Point p1(10.0, 20.0);
p1.print_info();

Geom::Point p2(3.5, 6.1);
p2.print_info();

std::cout << "\n--- 使用 Geom::Line ---" << std::endl;
// 创建 Line 对象。Line 同样在 Geom 命名空间内。
// Line 的构造函数需要两个 Point 对象,这些 Point 对象也必须是 Geom::Point 类型。
Geom::Line L1(p1, p2);
L1.print_info();

std::cout << "\n--- 使用 Geom::Cylinder ---" << std::endl;
// 创建 Cylinder 对象。Cylinder 也在 Geom 命名空间内。
Geom::Cylinder c1(1.4, 10.0); // 半径1.4,高10.0
std::cout << "Cylinder Volume: " << c1.volume() << std::endl;

std::cout << "\n--- 演示完毕 ---" << std::endl;
return 0;
}

解释:

  • main.cpp 中,我们包含了所有需要使用的类的头文件 (point.h, line.h, cylinder.h)。
  • 当我们要创建 PointLineCylinder 对象时,不能直接写 Point p1;,因为 Point 不在全局命名空间中。它存在于 Geom 命名空间里。
  • 因此,我们必须使用作用域解析运算符 :: 来访问它们,例如 Geom::PointGeom::LineGeom::Cylinder。这明确告诉编译器去 Geom 命名空间中查找这些类。

4. QA 闪卡 (QA Flash Cards)

  1. Q: 为什么C++需要引入命名空间这个特性?
    A: 主要是为了避免在大型项目中或者使用第三方库时,不同模块之间因定义了同名实体而造成的名称冲突

  2. Q: 当我在 file1.h 中写 namespace MyNamespace { ... },又在 file2.cpp 中写 namespace MyNamespace { ... } 时,这两个 MyNamespace 是两个不同的命名空间吗?
    A: 不是。C++会将它们视为同一个 MyNamespace 命名空间,并将其内部的所有声明和定义进行合并。

  3. Q: 如果一个类 MyClass 被定义在 MyNamespace 中,我在主函数中如何创建一个 MyClass 的对象?
    A: 需要使用作用域解析运算符 :: 来指定,例如 MyNamespace::MyClass obj;

  4. Q: 在同一个命名空间内部,比如 Geom 命名空间内的 Line 类中,如果我要使用 Geom 命名空间内的 Point 类,我还需要写 Geom::Point 吗?
    A: 不需要。在同一个命名空间内部,可以直接使用该命名空间内的其他实体,例如直接写 Point 即可。

5. 常见误解和错误 (Common Misunderstandings or Mistakes)

  1. 忘记命名空间前缀: 最常见的错误是尝试直接使用命名空间内的类或函数,例如在 main 函数中写 Point p1; 而不是 Geom::Point p1;。这会导致编译错误,因为编译器无法在全局命名空间中找到 Point
  2. 误以为跨文件是不同的命名空间: 初学者可能会认为 point.h 里的 namespace Geompoint.cpp 里的 namespace Geom 是独立的两个命名空间。实际上它们是同一个命名空间,并且会将其内容进行合并。理解这一点对于组织和管理代码至关重要。
  3. 命名空间嵌套的混淆: 虽然本节课没有深入,但未来可能会遇到命名空间嵌套(例如 namespace Outer { namespace Inner { ... } })。使用时需要完整路径 Outer::Inner::MyClass。混淆嵌套层级也会导致错误。
  4. 过度使用 using namespace 虽然可以使用 using namespace Geom; 来避免每次都写 Geom::,但这在头文件或者大型.cpp文件中被视为不良实践,因为它可能重新引入名称冲突,特别是在头文件中,这会影响到所有包含该头文件的文件。最佳实践是在需要的地方精确使用 Geom::,或者在函数内部(特别是主函数这种局部作用域)使用 using namespace

6. 编码练习 (Coding Exercise)

现在,轮到您来动手了!请完成以下任务,您需要填写缺失的代码,使程序能够编译并正确运行。

任务:
根据我们学到的知识,补全 main.cpp 中缺失的代码,使其能够创建并使用 Geom::PointGeom::Cylinder 对象。

请您完成 main.cpp 文件中标记为 TODO 的部分。

文件结构: (您不需要创建这些文件,假设它们已经存在并如上文所示)

  • point.h
  • point.cpp
  • cylinder.h
  • cylinder.cpp
  • main.cpp (您需要修改的部分)

point.h

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef GEOM_POINT_H
#define GEOM_POINT_H
namespace Geom {
class Point {
private:
double m_x;
double m_y;
public:
Point(double x_coord = 0.0, double y_coord = 0.0);
void print_info() const;
double get_x() const { return m_x; }
double get_y() const { return m_y; }
};
}
#endif

point.cpp

cpp
1
2
3
4
5
6
7
8
#include "point.h"
#include <iostream>
namespace Geom {
Point::Point(double x_coord, double y_coord) : m_x(x_coord), m_y(y_coord) {}
void Point::print_info() const {
std::cout << "Point: [x: " << m_x << ", y: " << m_y << "]" << std::endl;
}
}

cylinder.h

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef GEOM_CYLINDER_H
#define GEOM_CYLINDER_H
namespace Geom {
class Cylinder {
private:
double m_base_radius;
double m_height;
public:
Cylinder(double radius = 1.0, double height = 1.0);
double volume() const;
};
}
#endif

cylinder.cpp

cpp
1
2
3
4
5
6
7
8
9
10
11
12
#include "cylinder.h"
#include <iostream>
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace Geom {
Cylinder::Cylinder(double radius, double height) : m_base_radius(radius), m_height(height) {}
double Cylinder::volume() const {
return M_PI * m_base_radius * m_base_radius * m_height;
}
}

main.cpp (请修改这里)

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TODO: 1. 包含必要的头文件
#include <iostream>
// #include ...
// #include ...

int main() {
std::cout << "--- 编码练习 ---" << std::endl;

// TODO: 2. 创建一个名为 p1 的 Geom::Point 对象,坐标为 (5.0, 10.0)
// ??? p1(5.0, 10.0);
// TODO: 3. 调用 p1 的 print_info 方法
// ???

std::cout << "\n";

// TODO: 4. 创建一个名为 c1 的 Geom::Cylinder 对象,半径为 2.0,高度为 5.0
// ??? c1(2.0, 5.0);
// TODO: 5. 计算并打印 c1 的体积
// std::cout << "Cylinder volume: " << ??? << std::endl;

return 0;
}

请您填写完代码后提交,我会在您的下一个回复中给出评分、正确答案和进一步的建议。


评论